/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.mrroman.linksender.filesender.server;

import com.mrroman.linksender.filesender.sendables.Sendable;
import com.mrroman.linksender.filesender.sendables.SendableAdapter;
import com.mrroman.linksender.filesender.authenticator.HttpAuthenticator;
import com.mrroman.linksender.filesender.server.servlets.InputProcessor;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.net.SocketException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author gorladam
 */
public class ServerThread implements Runnable {

    private Logger logger = Logger.getLogger(ServerThread.class.getName());
    private String authenticatedUser;
    private long contentLength = -1;
    private String stringRequest;
    private String referer;
    private BufferedInputStream clientIn;
    private BufferedOutputStream clientOut;
    private InputStream inputStream;
    private Socket socket;
    private byte[] buffer = new byte[1024 * 64];
    private ServerThreadFactory serverThreadFactory;
    private Sendable root;

    public ServerThread(ServerThreadFactory settings, final Socket socket_, final Sendable root) {
        this.socket = socket_;
        this.serverThreadFactory = settings;
        this.root = root;
    }

    private static String divideStringRequest(String path) {
        StringBuilder result = new StringBuilder();
        StringBuilder prev = new StringBuilder();
        prev.append("/");
        result.append("<a href=\"/\">[root]</a>");
        for (String s : path.split("/")) {
            if (!"".equals(s)) {
                prev.append(EncodingTools.urlEncodeUTF(s));
                prev.append("/");
                result.append(" / <a href=\"" + prev.toString() + "\">" + s + "</a>");
            }
        }
        return result.toString();
    }

    private void sendUTF8(String text) throws IOException {
        String tmp = text + "\r\n";
        clientOut.write(tmp.getBytes("UTF-8"));
        //System.out.println(tmp.replace("\n", "\n<<< "));
    }

    private void sendHeader(String stringRequest) throws IOException {
        if (!"".equals(stringRequest)) {
            String title;
            title = serverThreadFactory.getServerName() + " - " + stringRequest;
            sendUTF8(serverThreadFactory.headBodyHTTP.replace(serverThreadFactory.getServerName(), title));
        } else {
            sendUTF8(serverThreadFactory.headBodyHTTP);
        }
        if (serverThreadFactory.getMenu() != null) {
            sendUTF8(serverThreadFactory.getMenu());
        }
        sendUTF8("<div id=\"CONTENT\">");
        clientOut.write(("<div id=\"HEADER\">" + divideStringRequest(stringRequest) + "</div>\r\n").getBytes("UTF-8"));
    }

    private void sendFooter(BufferedOutputStream clientOut) throws IOException {
        sendUTF8("\r\n<div id=\"FOTTER\">Generated by: <a href=\"" + serverThreadFactory.getServerHomepage() + "\">" + serverThreadFactory.getServerName() + "</a> on "
                + SimpleDateFormat.getDateTimeInstance().format(new Date()) + "</div>" + "\r\n");
        sendUTF8("</div></body></html>");
    }

    private static String readHeader(BufferedInputStream input) {
        int c1 = 0, c2 = 0, c3 = 0, c4 = 0;
        StringBuilder result = new StringBuilder(1024);
        try {
            do {
                c1 = c2;
                c2 = c3;
                c3 = c4;
                c4 = input.read();
                if (c4 > -1) {
                    result.append((char) c4);
                }
            } while (c4 > -1 && !((c1 == '\r' && c2 == '\n' && c3 == '\r' && c4 == '\n')
                    || (c3 == '\n' && c4 == '\n'))); // for linux nc compatibility
        } catch (IOException ex) {
            Logger.getLogger(ServerThreadFactory.class.getName()).log(Level.SEVERE, null, ex);
        }
        return result.toString();
    }

    private void sendStream(InputStream in, Integer rangeStart, Integer rangeStop) throws IOException {
        int count;
        if (rangeStop != null || rangeStart != null) {
            long send = 0;
            if (rangeStart != null) {
                in.skip(rangeStart);
                send = rangeStart;
            }
            if (rangeStop != null) {
                rangeStop++; // inclusive
            }
            while (true) {
                if (rangeStop != null && buffer.length + send > rangeStop) {
                    count = in.read(buffer, 0, (int) (-send + rangeStop));
                } else {
                    count = in.read(buffer);
                }
                if (count > 0) {
                    clientOut.write(buffer, 0, count);
                    send += count;
                } else {
                    break;
                }
            }
        } else {
            while (true) {
                count = in.read(buffer);
                if (count > 0) {
                    clientOut.write(buffer, 0, count);
                } else {
                    break;
                }
            }
        }
    }

    private void sendIcyStreamTitle(String title, int chunkSize) throws IOException {
        byte[] fileBuffer = new byte[chunkSize];
        Arrays.fill(fileBuffer, (byte) 0);
        String s = "StreamTitle='" + title + "';";
        int dlugosc = 1 + s.length() / 16;
        fileBuffer[0] = (byte) dlugosc;
        for (int i = 0; i < s.length(); i++) {
            fileBuffer[i + 1] = (byte) s.charAt(i);
        }
        clientOut.write(fileBuffer, 0, dlugosc * 16 + 1);
    }

    private void sendIcyStream(InputStream in, String name, int chunkSize) throws IOException {
        int n;
        boolean sendHeader = false;

        byte[] fileBuffer = new byte[chunkSize];
        try {
            int lastWrote = 0;
            while ((n = in.read(fileBuffer, 0, lastWrote < chunkSize ? chunkSize - lastWrote : chunkSize)) > 0) {
                clientOut.write(fileBuffer, 0, n);
                lastWrote += n;
                while (lastWrote >= chunkSize) {
                    lastWrote -= chunkSize;
                }
                if (lastWrote == 0) {
                    if (sendHeader) {
                        clientOut.write(0);
                    } else {
                        sendHeader = true;
                        sendIcyStreamTitle(name, chunkSize);
                    }
                } else {
                }
            }
            Arrays.fill(fileBuffer, (byte) 0);
            clientOut.write(fileBuffer, 0, chunkSize - lastWrote);
            clientOut.write(0);
        } finally {
        }
    }

    private boolean receiveFile() throws FileNotFoundException, IOException {
        long done = 0;
        String partHeader = readHeader(clientIn);
        File file = null;
        FileOutputStream fileOutputStream = null;
        for (String line : partHeader.split("\r\n")) {
            //System.out.println(">> " + line);
            if (line.toLowerCase().startsWith(HttpTools.CONTENTDISPOSITION.toLowerCase() + HttpTools.COLON)) {
                int pos1 = line.indexOf("filename=\"") + 10;
                int pos2 = line.indexOf("\";", pos1);
                if (pos2 < 0) {
                    pos2 = line.length() - 1;
                }
                String fileName = line.substring(pos1, pos2);
                for (String k : new String[]{"/", "\\", System.getProperty("file.separator")}) {
                    pos1 = fileName.lastIndexOf(k);
                    if (pos1 > 0) {
                        fileName = fileName.substring(pos1 + 1);
                    }
                }
                String decodedFileName = EncodingTools.urlDecodeUTF(fileName);
                file = new File(serverThreadFactory.getOutputDirectory() + decodedFileName);
                if (file.exists()) {
                    int i = 1;
                    int p = decodedFileName.lastIndexOf(".");
                    String k = null;
                    do {
                        k = (p < 1) ? decodedFileName + "." + i : decodedFileName.substring(0, p) + i + decodedFileName.substring(p);
                        file = new File(serverThreadFactory.getOutputDirectory() + k);
                        i++;
                    } while (file.exists());
                }
                fileOutputStream = new FileOutputStream(file);
                logger.log(Level.INFO, "receiving file \"" + file.getCanonicalPath() + "\" from " + socket.getInetAddress().getHostAddress() + (authenticatedUser == null ? "" : " - " + authenticatedUser));
            }
        }
        String splitter = "\r\n" + partHeader.substring(0, partHeader.indexOf("\r\n")) + "--\r\n";
        int splitterLength = splitter.length();
        if (fileOutputStream == null) {
            logger.log(Level.WARNING, "bad post request, invader: " + socket.getInetAddress().getHostAddress());
            sendUTF8(HttpTools.createHttpResponse(400, serverThreadFactory.getServerName(), true));
        } else {
            boolean ok = false;
            long fileSize = -1;
            try {
                done = partHeader.getBytes().length;
                int count = 0;
                long expected = contentLength - splitterLength;
                while (count != -1 && done < contentLength) {
                    int retrycount = 5;
                    while ((count = clientIn.read(buffer)) < 1) {
                        if (--retrycount < 0) {
                            break;
                        }
                        logger.log(Level.WARNING, "waiting for data, " + file.getCanonicalPath());
                        try {
                            Thread.sleep(5000);
                        } catch (InterruptedException ex) {
                        }
                    }
                    if (count > 0) {
                        if (done < expected) {
                            if (done + count > expected) {
                                fileOutputStream.write(buffer, 0, (int) (expected - done));
                            } else {
                                fileOutputStream.write(buffer, 0, count);
                            }
                        }
                        done += count;
                    } else {
                        break;
                    }
                }
                fileSize = done - partHeader.getBytes().length - splitterLength;
                fileOutputStream.close();
                fileOutputStream = null;
                long realFileSize = file.length();
                ok = (done == contentLength) && (fileSize == realFileSize);
            } finally {
                String back = referer != null ? referer : stringRequest != null ? stringRequest : "/";
                if (ok) {
                    String message = "succesfully received file \"" + file.getName() + "\" (" + fileSize + " bytes) from " + socket.getInetAddress().getHostAddress() + (authenticatedUser == null ? "" : " - " + authenticatedUser);
                    logger.log(Level.INFO, message);
                    sendUTF8(HttpTools.createHttpResponse(200, serverThreadFactory.getServerName(), -1, HttpTools.CONTENTTYPE_TEXT_HTML));
                    sendHeader(stringRequest);
                    sendUTF8("<h3>OK</h3><p>" + message + "</p><p><a href=\"" + back + "\">send more...</a></p>");
                } else {
                    String message = "failed receiving file \"" + file.getName() + "\" from " + socket.getInetAddress().getHostAddress() + (authenticatedUser == null ? "" : " - " + authenticatedUser);
                    logger.log(Level.SEVERE, message);
                    sendUTF8(HttpTools.createHttpResponse(400, serverThreadFactory.getServerName(), -1, HttpTools.CONTENTTYPE_TEXT_HTML));
                    sendHeader(stringRequest);
                    sendUTF8("<h3>ERROR</h3><p>" + message + "</p><p><a href=\"" + back + "\">send more...</a></p>");
                }
                sendFooter(clientOut);
                if (fileOutputStream != null) {
                    fileOutputStream.close();
                }
                return ok;
            }
        }
        return false;
    }

    @Override
    public void run() {
        try {
            clientIn = new BufferedInputStream(socket.getInputStream());
            Sendable response = null;
            HttpAuthenticator authenticator = null;
            String authorization = null;
            Integer rangeStart = null;
            Integer rangeStop = null;
            int httpResultCode = 0;
            boolean headRequest = false;
            boolean getRequest = false;
            boolean postRequest = false;
            String requestHeader = null;
            String contentType = null;
            String host;
            boolean icyMetaData = false;

            requestHeader = readHeader(clientIn);
            if (requestHeader.startsWith("GET ")) {
                getRequest = true;
            } else if (requestHeader.startsWith("HEAD ")) {
                headRequest = true;
            } else if (requestHeader.startsWith("POST ")) {
                postRequest = true;
            }

            int lineCounter = -1;
            //System.out.println("-----");
            if (requestHeader.length() > 0) {
                for (String line : requestHeader.split("\r\n")) {
                    lineCounter++;
                    //System.out.println("> " + line);
                    if (lineCounter == 0) {
                        if (getRequest || headRequest || postRequest) {
                            stringRequest = line.substring(5, line.lastIndexOf(" "));
                            stringRequest = EncodingTools.urlDecodeUTF(stringRequest);
                        } else {
                            httpResultCode = 400;
                        }
                    } else {
                        String lineToLowerCase = line.toLowerCase();
                        if (lineToLowerCase.startsWith(HttpTools.AUTHORIZATION.toLowerCase() + HttpTools.COLON)) {
                            authorization = line.substring(HttpTools.AUTHORIZATION.length() + 1).trim();
                        } else if (lineToLowerCase.startsWith(HttpTools.RANGE.toLowerCase() + HttpTools.COLON)) {
                            String range = lineToLowerCase.substring(HttpTools.RANGE.length() + 1).trim();
                            int pos1 = range.indexOf('=');
                            int pos2 = range.indexOf('-');
                            int posmulti = range.indexOf(','); // multi ranges are not supported
                            if (range.startsWith("bytes") && pos1 > 0 && pos2 > pos1 && posmulti == -1) {
                                String sstart = range.substring(pos1 + 1, pos2).trim();
                                String sstop = range.substring(pos2 + 1).trim();
                                try {
                                    if (!sstart.equals("")) {
                                        rangeStart = Integer.valueOf(sstart);
                                    }
                                    if (!sstop.equals("")) {
                                        rangeStop = Integer.valueOf(sstop); //range is inclusive
                                    }
                                } catch (NumberFormatException e) {
                                    rangeStart = null;
                                    rangeStop = null;
                                }
                            } else {
                                httpResultCode = 416;
                            }
                        } else if (lineToLowerCase.startsWith(HttpTools.CONTENTLENGTH.toLowerCase() + HttpTools.COLON)) {
                            String cl = lineToLowerCase.substring(HttpTools.CONTENTLENGTH.length() + 1).trim();
                            if (!cl.equals("")) {
                                contentLength = Long.valueOf(cl);
                            }
                        } else if (lineToLowerCase.startsWith(HttpTools.CONTENTTYPE.toLowerCase() + HttpTools.COLON)) {
                            contentType = lineToLowerCase.substring(HttpTools.CONTENTTYPE.length() + 1).trim();
                        } else if (lineToLowerCase.startsWith(HttpTools.REFERER.toLowerCase() + HttpTools.COLON)) {
                            referer = lineToLowerCase.substring(HttpTools.REFERER.length() + 1).trim();
                        } else if (lineToLowerCase.startsWith(HttpTools.HOST.toLowerCase() + HttpTools.COLON)) {
                            host = lineToLowerCase.substring(HttpTools.HOST.length() + 1).trim();
                        } else if (lineToLowerCase.startsWith(HttpTools.ICYMETADATA.toLowerCase() + HttpTools.COLON)) {
                            String value = lineToLowerCase.substring(HttpTools.ICYMETADATA.length() + 1).trim();
                            icyMetaData = "1".equals(value);
                        }
                    }
                    if (httpResultCode > 0) {
                        break;
                    }
                }
            }

            if (getRequest || headRequest) {
                response = SendableAdapter.tryToFindSendable(root, stringRequest);
            }
            authenticator = response == null ? null : response.getAuthenticator();
            if (authenticator == HttpTools.NULLAUTHENTICATOR) {
                authenticator = null;
            }
            authenticatedUser = (authorization == null || authenticator == null) ? null : authenticator.allowed(authorization);

            clientOut = new BufferedOutputStream(socket.getOutputStream());
            if (httpResultCode > 0) {
                sendUTF8(HttpTools.createHttpResponse(httpResultCode, serverThreadFactory.getServerName(), true));
            } else if (stringRequest == null || (postRequest && (contentLength < 1 || contentType == null))) {
                logger.log(Level.WARNING, "bad request, invader: " + socket.getInetAddress().getHostAddress());
                sendUTF8(HttpTools.createHttpResponse(400, serverThreadFactory.getServerName(), true));
            } else if (!icyMetaData && (getRequest || headRequest) && authenticator != null && authenticatedUser == null) {
                logger.log(Level.WARNING, "not authorized, invader: " + socket.getInetAddress().getHostAddress());
                sendUTF8(HttpTools.createHttpResponse(401, HttpTools.WWWAUTHENTICATE, authenticator.getAuthenticate()));
            } else if (response == null && (getRequest || headRequest)) {
                logger.log(Level.WARNING, "not found: " + stringRequest + ", invader: " + socket.getInetAddress().getHostAddress());
                sendUTF8(HttpTools.createHttpResponse(404, serverThreadFactory.getServerName(), true));
            } else if (icyMetaData && !"audio/mpeg".equals(response.getMimeType())) {
                logger.log(Level.WARNING, "streaming forbidden, invader: " + socket.getInetAddress().getHostAddress());
                sendUTF8(HttpTools.createHttpResponse(403, serverThreadFactory.getServerName(), true));
            } else if ((getRequest || headRequest) && !response.isRawFile() && !stringRequest.equals("") && !stringRequest.endsWith("/")) {
                logger.log(Level.INFO, "redirecting to /" + stringRequest + "/");
                sendUTF8(HttpTools.createHttpResponse(301, HttpTools.LOCATION, "/" + stringRequest + "/"));
            } else if ((getRequest || headRequest) && response.getFilter() != null && !response.getFilter().isAllowed(response, socket.getInetAddress().getHostAddress(), authenticatedUser)) {
                logger.log(Level.WARNING, "not allowed, invader: " + socket.getInetAddress().getHostAddress());
                sendUTF8(HttpTools.createHttpResponse(403, serverThreadFactory.getServerName(), true));
            } else {
                if (response instanceof InputProcessor) {
                    ((InputProcessor) response).setRequestHeader(requestHeader);
                    ((InputProcessor) response).setRequestStream(clientIn);
                }
                inputStream = (getRequest || headRequest) ? response.getResponseStream() : null;
                if ((getRequest || headRequest) && (inputStream == null)) {
                    logger.log(Level.WARNING, "not found: " + response + ", invader: " + socket.getInetAddress().getHostAddress());
                    sendUTF8(HttpTools.createHttpResponse(404, serverThreadFactory.getServerName(), true));
                } else {
                    if (!postRequest) {
                        logger.log(Level.INFO, (icyMetaData ? "streaming" : "sending") + " \"" + stringRequest + "\" to " + socket.getInetAddress().getHostAddress() + (authenticatedUser == null ? "" : " - " + authenticatedUser));
                    }
                    BufferedInputStream bis = new BufferedInputStream(inputStream);
                    if (getRequest || headRequest) {
                        boolean raw = response.isRawFile();
                        if (icyMetaData && raw) {
                            sendUTF8(HttpTools.createIcecastResponse(serverThreadFactory.getServerName(), 1024 * 16));
                        } else if (raw && response instanceof HasHtmlHeaders) {
                            sendUTF8(((HasHtmlHeaders) response).getHtmlHeader());
                        } else if (rangeStart != null || rangeStop != null) {
                            sendUTF8(HttpTools.createHttpResponse(206, serverThreadFactory.getServerName(), (rangeStart == null ? -1 : rangeStart), (rangeStop == null ? -1 : rangeStop), response.getContentLength(), response.getMimeType()));
                        } else {
                            sendUTF8(HttpTools.createHttpResponse(200, serverThreadFactory.getServerName(), response.getContentLength(), response.getMimeType()));
                        }
                        if (getRequest) {
                            if (!raw) {
                                sendHeader(stringRequest);
                            }
                            if (icyMetaData) {
                                sendIcyStream(bis, response.toString(), 1024 * 16);
                            } else {
                                if (response.getContentLength() > 0 && rangeStop == null) {
                                    rangeStop = new Integer((int)response.getContentLength()-1);
                                }
                                sendStream(bis, rangeStart, rangeStop);
                            }
                            if (!raw) {
                                sendFooter(clientOut);
                            }
                        }
                    } else if (postRequest) {
                        socket.setReceiveBufferSize(buffer.length);
                        socket.setSoTimeout(5000);
                        receiveFile();
                    }
                }
            }
            clientOut.flush();
        } catch (SocketException ex) {
            logger.log(Level.SEVERE, ex.getMessage());
        } catch (IOException ex) {
            logger.log(Level.SEVERE, null, ex);
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException ex) {
                    logger.log(Level.SEVERE, null, ex);
                }
            }
            if (clientOut != null) {
                try {
                    clientOut.close();
                } catch (IOException ex) {
                    logger.log(Level.SEVERE, null, ex);
                }
            }
            try {
                socket.close();
            } catch (IOException ex) {
                logger.log(Level.SEVERE, null, ex);
            }
        }

    }
}
